在学习 JavaScript 的时候,很多人都会遇到这样一个现象:
jsCopy
sum = 1 + 2;
console.log(sum); // 3
这段代码竟然可以正常运行,即使我们没有用 var、let 或 const 去声明 sum。
但是下面这段代码却会直接报错:
jsCopy
console.log(test); // ReferenceError: test is not defined
同样都是“没有声明”,为什么一个能用,一个会报错?
这背后其实涉及 JavaScript 中几个非常重要的概念:
变量声明与赋值
作用域查找
隐式全局变量
严格模式
ReferenceError的触发机制
下面我们系统地讲清楚这个问题。
一、先看结论
先直接给出结论:
1. sum = 1 + 2 为什么能执行?
因为这不是“读取一个未定义变量”,而是给一个未声明的标识符赋值。
在非严格模式下,JavaScript 会帮你在全局对象上创建一个同名属性,相当于偷偷生成了一个全局变量。
jsCopy
sum = 1 + 2;
console.log(sum); // 3
console.log(window.sum); // 浏览器中也是 3
2. console.log(test) 为什么报错?
因为这里是在读取一个根本不存在的变量。
JavaScript 在当前作用域和外层作用域一路查找,都找不到 test,于是抛出:
jsCopy
ReferenceError: test is not defined
二、赋值和读取,其实不是一回事
很多初学者会把这两种情况都理解为“变量没声明”,但 JavaScript 对它们的处理方式完全不同。
情况 1:对未声明变量进行赋值
jsCopy
sum = 1 + 2;
这里的动作是:赋值。
在非严格模式下,JS 会认为你想在全局环境里创建一个变量,于是帮你做了这件事。
相当于:
jsCopy
window.sum = 3; // 浏览器环境
情况 2:直接读取未声明变量
jsCopy
console.log(test);
这里的动作是:读取。
JS 引擎会去作用域链里查找 test:
当前作用域有没有?
外层作用域有没有?
全局作用域有没有?
如果都没有,就说明这个标识符根本不存在,于是直接报错。
三、隐式全局变量:sum = 1 + 2 的本质
来看这段代码:
jsCopy
function foo() {
sum = 1 + 2;
}
foo();
console.log(sum); // 3
虽然 sum 写在函数内部,但由于没有声明,在非严格模式下,sum 会变成一个隐式全局变量。
这意味着:
它不是函数私有的
它会污染全局作用域
项目稍微一大就容易引发命名冲突
例如:
jsCopy
function a() {
total = 100;
}
function b() {
total = 200;
}
a();
console.log(total); // 100
b();
console.log(total); // 200
这里的 total 就成了一个任何地方都能改的全局变量,非常危险。
四、为什么读取不存在的变量会报 ReferenceError
看这个例子:
jsCopy
console.log(test);
JS 在执行时需要先拿到 test 对应的值。
但如果这个变量根本没有被声明过,那么引擎连“这个名字对应哪块内存”都不知道,自然无法继续执行。
于是抛出:
jsCopy
ReferenceError: test is not defined
这里的关键点是:
未声明:根本不存在这个变量
未赋值:变量声明了,但值可能是
undefined
这两个概念千万不要混淆。
五、undefined 和 not defined 的区别
这是这个问题最容易延伸出的另一个知识点。
1. undefined:变量存在,但值是 undefined
jsCopy
var a;
console.log(a); // undefined
说明:
a已经声明了只是还没赋值
2. not defined:变量根本没声明
jsCopy
console.log(b); // ReferenceError: b is not defined
说明:
b根本不存在引擎找不到它
对比一下
jsCopy
var a;
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
所以:
undefined是一个值not defined是一种错误状态
六、严格模式下,sum = 1 + 2 也会报错
上面说的“直接赋值能用”,有一个前提:非严格模式。
如果开启严格模式:
jsCopy
'use strict';
sum = 1 + 2; // ReferenceError: sum is not defined
为什么?
因为严格模式禁止创建隐式全局变量。
也就是说,JS 不再帮你“偷偷创建变量”,而是要求你必须明确声明。
这也是现代 JavaScript 推荐的写法。
七、浏览器里为什么会挂到 window 上?
在浏览器环境中,全局作用域和 window 对象有密切关系。
比如:
jsCopy
var a = 1;
console.log(window.a); // 1
而对于非严格模式下的隐式全局变量:
jsCopy
b = 2;
console.log(window.b); // 2
所以很多时候你会看到:
var声明的全局变量,会成为window的属性未声明直接赋值生成的隐式全局变量,也会成为
window的属性
但是要注意,let 和 const 不一样:
jsCopy
let c = 3;
const d = 4;
console.log(window.c); // undefined
console.log(window.d); // undefined
它们虽然处于全局作用域,但不会成为 window 的属性。
八、从执行机制上理解这个现象
如果从更底层一点的角度理解,可以把 JS 的处理过程分成两类:
1. 读取标识符
比如:
jsCopy
console.log(test);
引擎会做变量解析:
查找
test找不到
抛
ReferenceError
2. 给标识符赋值
比如:
jsCopy
sum = 3;
引擎会先尝试查找 sum 对应的引用。
如果找到了,就赋值
如果没找到:
非严格模式:创建一个全局属性
严格模式:抛
ReferenceError
所以“读取”和“赋值”虽然都涉及变量名,但底层行为并不一样。
九、实际开发中为什么要避免这种写法
虽然下面这种代码能跑:
jsCopy
sum = 1 + 2;
但在工程实践里,这是非常不推荐的写法,原因包括:
1. 污染全局作用域
变量会泄漏到全局,影响整个应用。
2. 容易产生命名冲突
多个模块可能不小心使用同名变量。
3. 难以排查 Bug
你以为是局部变量,实际上它跑到了全局。
4. 严格模式下直接报错
现代前端工程中,大多数代码都会默认运行在更严格的环境中。
5. 不利于代码维护
团队协作时,隐式行为会让代码意图不清晰。
十、正确写法是什么?
始终使用 let、const 或 var 明确声明变量。
推荐写法
jsCopy
const sum = 1 + 2;
console.log(sum);
如果变量后续会变化:
jsCopy
let total = 0;
total = total + 10;
除非是兼容旧代码,否则现在一般不推荐优先使用 var。
十一、面试里可以怎么回答这个问题
如果面试官问:
为什么
sum = 1 + 2可以执行,而console.log(test)会报错?
你可以这样回答:
因为两者本质上不是同一种操作。
sum = 1 + 2是给一个未声明变量赋值,在非严格模式下,JavaScript 会自动把它创建为一个隐式全局变量;
而console.log(test)是读取一个未声明的变量,JS 引擎在作用域链中找不到这个标识符时,就会抛出ReferenceError。
如果开启严格模式,那么对未声明变量赋值也会报错。
这个回答就比较完整了。
十二、总结
最后再总结一遍:
sum = 1 + 2 能运行的原因
它是“赋值操作”
在非严格模式下
JS 会创建隐式全局变量
console.log(test) 报错的原因
它是“读取操作”
test没有被声明作用域链中找不到
所以抛出
ReferenceError
最佳实践
永远使用
let/const声明变量避免隐式全局变量
使用严格模式或现代工程化工具避免这类问题
十三、一个完整示例
jsCopy
// 非严格模式
function demo() {
sum = 1 + 2; // 隐式全局变量
console.log(sum); // 3
}
demo();
console.log(sum); // 3
console.log(window.sum); // 浏览器中为 3
console.log(test); // ReferenceError: test is not defined
严格模式下:
jsCopy
'use strict';
function demo() {
sum = 1 + 2; // ReferenceError: sum is not defined
}
demo();